import threading
from uuid import uuid4 as uuid
from collections import deque

from classact import local
from classact.actor import Actor
from classact.log import ctrl_logger, NullFile
from classact.transport import Response, DeliveryError

class Controller(object):
    
    # IDs are generated using UUIDs, to ensure very low chance of collision.
    id = uuid().hex
    # Dictionary of actors controlled by this instance.
    actors = {}
    # Message and response queues.
    msg_queue = deque()
    response_queue = deque()

    def __init__(self, logfile=NullFile(), *args, **kwargs):
        # Dictionary of controllers known by this instance.
        self.controllers = {self.id: self}
        # The message and response handlers co-ordinate the movement of (as you
        # can guess) messages and responses between actors on this controller.
        # They also allow for actors on different, connected controllers to
        # speak to one another.
        self.msg_handler = MsgHandler(self)
        self.response_handler = ResponseHandler(self)
        self.logger = ctrl_logger(self, logfile)
        # Log initialization of this controller.
        self.logger.info('CTRL(%s) || Controller initiated', self.id)

    def spawn_actor(self, actor_class):
        assert issubclass(actor_class, Actor), \
            'Class "%r" is not a subclass of Actor' % (actor_class,)
        # By default, the ``Actor.__init__`` method will configure the
        # registration of the actor on this controller instance. The actor must
        # be started manually by this procedure too.
        actor_instance = actor_class(self)
        actor_instance.start()
        # ID is returned to the caller who asked for the actor to be spawned.
        return actor_instance.id

    def spawn_controller(self, controller_class=None, *args, **kwargs):
        # By default, spawn another of the same class.
        if controller_class is None:
            controller_class = self.__class__
        # The instance's class is held in the ``self.__class__`` attribute.
        # Classes have a method, ``mro``, which returns the list of inherited
        # classes for use when resolving class method and attribute names. This
        # list can also be used to find the parent ``Controller`` class, as it
        # will be the class just before ``object`` in the list.
        mro = self.__class__.mro()
        parent_ctrl_cls = mro[mro.index(object) - 1]
        assert issubclass(controller_class, parent_ctrl_cls), \
            'Class "%r" is not a subclass of ``Controller``' % \
                (controller_class,)
        # Initialize the instance, giving it any extra arguments passed to this
        # method.
        controller_instance = controller_class(*args, **kwargs)
        controller_instance.start()
        # The ``Controller`` class does not handle registration on
        # initialization (this behaviour is to allow ``Controller`` instances
        # to be created without a parent), so it is left up to this method to
        # do so. The ID is returned to the caller who asked for the controller
        # to be spawned.
        return self.register_controller(controller_instance)

    def register_controller(self, controller_instance):
        assert isinstance(controller_instance, self.__class__), \
            'Object "%r" is not an instance of Controller' \
                % (controller_instance,)
        # Set up two-way communication links using local controller proxies.
        self.controllers[controller_instance.id] = \
            local.ControllerProxy(controller_instance)
        controller_instance.controllers[self.id] = local.ControllerProxy(self)
        # Give controller ID back to the caller. This is not strictly
        # necessary, but if the caller receives the ID, then at least it means
        # nothing went wrong along the way.
        return controller_instance.id

    def lookup_actor(self, sender, actor_id):
        # If the actor is controlled by this controller instance, return a
        # local actor proxy.
        if actor_id in self.actors:
            return local.ActorProxy(sender, self.actors[actor_id])
        # Otherwise, look up and find the first occurrence in one of the known
        # controller instances.
        else:
            controller_ids = self.controllers.keys()
            controller_ids.remove(self.id)
            controllers = map(self.controllers.get, controller_ids)
            for ctrl in controllers:
                # Ask all known controllers to look up the specified actor id.
                # This is recursive, as (hopefully!) all controllers will have
                # the same definition of ``lookup_actor``.
                actor_proxy = ctrl.lookup_actor(actor_id)
                # If nothing is found by a controller, it will return ``None``;
                # otherwise it will return the desired actor proxy, which will
                # then be returned to the caller here.
                if actor_proxy is not None:
                    return actor_proxy
            # If nothing is found, return ``None``.
            return None

    def lookup_controller(self, ctrl_id):
        # This exhibits similar behavior to the ``lookup_actor`` method; search
        # locally known controllers and then recur down the visible network,
        # returning the first found occurrence.
        if ctrl_id in self.controllers:
            return local.ControllerProxy(self.controllers[ctrl_id])
        else:
            controller_ids = self.controllers.keys()
            controller_ids.remove(self.id)
            controllers = map(self.controllers.get, controller_ids)
            for ctrl in controllers:
                ctrl_proxy = ctrl.lookup_controller(ctrl_id)
                if ctrl_proxy is not None:
                    return ctrl_proxy
            return None

    def register_actor(self, actor):
        assert isinstance(actor, Actor), \
            'Object "%r" is not an instance of Actor' % (actor,)
        actor_id = uuid.uuid4().hex
        # Because the actor has already been spawned, it is only necessary to
        # insert it into the controller instance's collection of known actors.
        self.actors[actor_id] = actor
        # No check is put in place to determine whether or not the actor is
        # alive; this is because the ``Actor.__init__`` method registers the
        # actor before calling ``threading.Thread.__init__``. Therefore,
        # calling actor.start() would produce an exception. This doesn't matter
        # much anyway; actors should be spawned through the
        # ``Controller.spawn_actor`` method, which starts them automatically
        # after they are initialized.
        return actor_id

    def register_msg(self, msg):
        assert isinstance(msg, Message), \
            'Object "%r" is not an instance of Message' % (msg,)
        msg_id = uuid.uuid4().hex
        # Here, it is necessary to assign the message instance's ``id``
        # attribute; this is because it is automatically inserted into the
        # message queue, and so it needs an ID before this method can insert
        # it.
        msg.id = msg_id
        # Add the message to the controller instance's message queue.
        self.msg_queue.appendleft(msg)
        # Return the message's ID to the caller, so that it may log it and use
        # it to watch for the response.
        return msg_id

    def register_response(self, response):
        assert isinstance(response, Response), \
            'Object "%r" is not an instance of Response' % (response,)
        response_id = uuid.uuid4().hex
        # See the ``register_msg`` method for why it is necessary to assign the
        # response instance an ID in this method.
        response.id = response_id
        self.response_queue.appendleft(msg)
        # Return the response's ID to the caller, so that it may log it, etc.
        return response_id

    def start(self):
        # Log that the controller has been started.
        self.log.info('CTRL(%s) || Controller started', self.id)
        # Starting the controller is as simple as starting it's message and
        # response handlers.
        self.msg_handler.start()
        self.response_handler.start()

class MsgHandler(threading.Thread):

    def __init__(self, controller, *args, **kwargs):
        self.controller = controller
        # Log initialization of the message handler on the parent controller's
        # log, for informational purposes.
        self.controller.log.info('CTRL(%s) || Message handler initiated',
            self.controller.id)
        # The following is required for all ``threading.Thread`` subclasses
        # which extend the ``__init__`` method.
        threading.Thread.__init__(self, *args, **kwargs)

    def run(self):
        # Log startup of message handler on controller instance's log.
        self.controller.log.info('CTRL(%s) || Message handler started',
            self.controller.id)
        # Begin main message handling loop.
        while True:
            if len(self.controller.msg_queue) > 0:
                # Get the first message from the controller instance's message
                # queue, ready for handling.
                curr_msg = self.controller.msg_queue.pop()
                # The following try-except clause is simply so that any
                # unexpected exceptions can be caught and logged, for debugging
                # purposes.
                try:
                    # The message holds the id of the recipient, so look up the
                    # actor using the parent controller instance. A sender of
                    # None can be used, as the returned proxy will not be used
                    # to call actor methods; it is only for accessing the
                    # actor's message queue.
                    rcpt_proxy = self.controller.lookup_actor(None,
                        curr_msg.rcpt)
                    # A ``rcpt_proxy`` of ``None`` means the rcpt ID was not
                    # found.
                    if rcpt_proxy is None:
                        # Log this as an error
                        self.controller.log.error(
                            'MSG(%s) || RCPT(%s) || Recipient not found',
                            curr_msg.id, curr_msg.rcpt)
                        # Now create a response which tells the original sender
                        # that the message sending has failed.
                        exc = DeliveryError(
                            'MSG(%s) || RCPT(%s) || Recipient not found' % (
                                curr_msg.rcpt,))
                        resp = Response(curr_msg, exception=exc)
                        # Register the response on the controller, and keep on
                        # processing. The response will be picked up by the
                        # controller's response handler and sent back to the
                        # sender, who will raise the response.
                        self.controller.register_response(resp)
                        continue
                    else:
                        # Add the current message to the recipient's message
                        # queue, through the proxy's transparent interface.
                        # The ``appendleft`` method is used because this is a
                        # queue; messages are obtained using ``pop``, which
                        # removes them from the right, therefore they should be
                        # appended to the left. See ``collections.deque``.
                        rcpt_proxy.msg_queue.appendleft(curr_msg)
                        # Log this, for information. Such information should
                        # not be logged on large throughput systems, so it is
                        # only recorded at the 'DEBUG' level (the lowest log
                        # level).
                        self.controller.log.debug(
                            'MSG(%s) || RCPT(%s)',
                            curr_response.id, curr_response.rcpt)
                        continue
                # Log any unexpected exceptions or warnings, but continue
                # working anyway.
                except Exception, e:
                    self.controller.log.error(
                        'MSG(%s) || RCPT(%s) || %s: %s',
                        curr_msg.id, curr_msg.rcpt, e.__class__.__name__,
                            e.message)
                    resp = Response(curr_msg, exception=e)
                except Warning, w:
                    self.controller.log.warning(
                        'MSG(%s) || RCPT(%s) || %s: %s',
                        curr_msg.id, curr_msg.rcpt, w.__class__.__name__,
                            w.message)
                    resp = Response(curr_msg, exception=w)
                # By this time, if everything had gone smoothly the loop would
                # have continued already, so we can assume that, if control
                # reaches this point, an unidentified error *must* have
                # occurred. Therefore, we should use the generated exceptional
                # response and return it to the sender.
                self.controller.register_response(resp)
                continue

class ResponseHandler(threading.Thread):
    def __init__(self, controller, *args, **kwargs):
        self.controller = controller
        # Log initialization of the response handler on the paren
        # controller's log, for informational purposes.
        self.controller.log.info('CTRL(%s) || Response handler initialized',
            self.controller.id)
        # The following is required for all ``threading.Thread`` subclasses
        # which extend the ``__init__`` method.
        threading.Thread.__init__(self, *args, **kwargs)

    def run(self):
        # Log startup of response handler on controller instance's log.
        self.controller.log.info('CTRL(%s) || Response handler started',
            self.controller.id)
        # Begin main response handling loop. Works in a similar way to the
        # message handler, only it acts upon the response queues, and the log
        # messages are slightly different. In addition, errors which occur here
        # should not be sent back to the sender; the process which generated
        # them will have exited already. However, exceptions may be logged.
        # See the comments on the ``MsgHandler.run`` method for more
        # information.
        while True:
            if len(self.controller.response_queue) > 0:
                curr_response = self.controller.response_queue.pop()
                try:
                    rcpt_proxy = self.controller.lookup_actor(None,
                        curr_response.rcpt)
                    if rcpt_proxy is None:
                        self.controller.log.error(
                            'RESP(%s) || RCPT(%s) || Recipient not found',
                            curr_response.id, curr_response.rcpt)
                    else:
                        rcpt_proxy.response_queue.appendleft(curr_response)
                        # Log return value and exceptions raised (if any).
                        if curr_response.exception is not None:
                            exc = curr_response.exception
                            if isinstance(exc, Warning):
                                exc_type = 'WARNING'
                            else:
                                exc_type = 'ERROR'
                            self.controller.log.debug(
                                'RESP(%s) || RCPT(%s) || %s || %s: %s',
                                curr_response.id, curr_response.rcpt,
                                exc_type, exc.__class__.__name__, exc.message)
                        self.controller.log.debug(
                            'RESP(%s) || RCPT(%s) || RETURN || %r',
                            curr_response.id, curr_response.rcpt,
                            curr_response.value)
                except Exception, e:
                    self.controller.log.error(
                        'RESP(%s) || RCPT(%s) || %s: %s',
                        curr_response.id, curr_response.rcpt,
                        e.__class__.__name__, e.message)
                except Warning, w:
                    self.controller.log.warning(
                        'RESP(%s) || RCPT(%s) || %s: %s',
                        curr_response.id, curr_response.rcpt,
                        w.__class__.__name__, w.message)